#include <stdio.h>
#include <SDL.h>
#include <GL/glew.h>
#include <Camera/FlyCamera.h>
#include <Core/Assert.h>
#include <Core/Types.h>
#include <Graphics/Device.h>
#include <Graphics/FrameBuffer.h>
#include <Graphics/IndexBuffer.h>
#include <Graphics/RenderTarget.h>
#include <Graphics/Shader.h>
#include <Graphics/Texture.h>
#include <Graphics/VertexBuffer.h>
#include <Maths/Constants.h>
#include <Maths/Projections.h>
#include <Maths/Vector2.h>
#include <Maths/Vector3.h>
#include <Input/Keyboard.h>
#include <Input/Mouse.h>


const size_t kScreenWidth = 1280;
const size_t kScreenHeight = 720;
const float kAspectRatio = (float)kScreenWidth / (float)kScreenHeight;

const float encloseUnitSphereVertices[36] =
{
    +0.661591f, +1.070477f, +0.000000f,  -0.661591f, +1.070477f, +0.000000f,  
    +0.000000f, +0.661591f, +1.070477f,  -1.070477f, +0.000000f, +0.661591f,  
    +0.000000f, -0.661591f, +1.070477f,  +1.070477f, +0.000000f, +0.661591f,  
    +1.070477f, +0.000000f, -0.661591f,  +0.661591f, -1.070477f, +0.000000f,  
    -0.661591f, -1.070477f, +0.000000f,  +0.000000f, -0.661591f, -1.070477f,  
    +0.000000f, +0.661591f, -1.070477f,  -1.070477f, +0.000000f, -0.661591f,  
};

const unsigned __int32 triangleIndices[60] =
{
    0, 1, 2, 2, 1, 3, 2, 3, 4, 
    5, 2, 4, 0, 2, 5, 0, 5, 6, 
    6, 5, 7, 5, 4, 7, 7, 4, 8, 
    7, 8, 9, 7, 9, 6, 6, 9, 10, 
    10, 9, 11, 10, 11, 1, 10, 1, 0, 
    6, 10, 0, 11, 3, 1, 8, 3, 11, 
    4, 3, 8, 11, 9, 8, 
};

GLenum err = 0;

VertexBuffer* fullscreenVb = NULL;
VertexBuffer* modelVb = NULL;
VertexBuffer* floorVb = NULL;
IndexBuffer* modelIb = NULL;

VertexBuffer* lightVb = NULL;
IndexBuffer* lightIb = NULL;

Shader* testShader = NULL;
Shader* fullscreenCopyShader = NULL;
Shader* pointLightShader = NULL;

FrameBuffer* lightFb = NULL;
FrameBuffer* geometryFb = NULL;
Texture* normalRt = NULL;
Texture* linearDepthRt = NULL;
Texture* lightRt = NULL;
RenderTarget* depthRt = NULL;

struct Light
{
    Vector3 colour;
    Vector3 position;
    Vector3 velocity;
    float life;
    float radius;
    float extent;
};

static const size_t kMaxLightCount = 512;
Light lights[kMaxLightCount];
size_t lightBegin = 0;
size_t lightCount = 0;
bool isPaused = false;


// GLuint query = 0;

FlyCamera camera(Vector3(0, 0.1f, 0.5f), Vector3::kZero);


bool isDone = false;


struct Header
{
    unsigned __int32 magic;
    unsigned __int32 version;
    unsigned __int32 elementCount;
    unsigned __int32 vertexStride;
    unsigned __int32 vertexCount;
    unsigned __int32 indexStride;
    unsigned __int32 indexCount;
    unsigned __int32 reserved0;
};


void Align(FILE* file, size_t alignment)
{
    size_t pos = ftell(file);
    size_t mask = alignment - 1;
    pos = (pos + mask) & ~mask;
    fseek(file, pos, SEEK_SET);
}

void LoadMesh(const char* path, VertexBuffer*& vb, IndexBuffer*& ib)
{
    FILE* file = fopen(path, "rb");
    Assert(NULL != file, "Failed to open mesh file '%s'", path);
    Header header;
    fread(&header, sizeof(header), 1, file);
    Align(file, 16);

    Assert(header.magic == 'MESH', "Expected MESH");
    Assert(header.version == 0, "Bad mesh version");
    Assert(header.elementCount < VertexElement::kAttributeCount, "Too many elements");

    VertexElement elements[VertexElement::kAttributeCount];
    fread(elements, sizeof(VertexElement), header.elementCount, file);
    Align(file, 16);

    void* vertexData = malloc(header.vertexStride*header.vertexCount);
    fread(vertexData, header.vertexStride, header.vertexCount, file);
    Align(file, 16);

    void* indexData = malloc(header.indexStride*header.indexCount);
    fread(indexData, header.indexStride, header.indexCount, file);

    fclose(file);

    vb = new VertexBuffer(VertexBuffer::kStatic, elements, header.elementCount, header.vertexStride, header.vertexCount);
    ib = new IndexBuffer(IndexBuffer::kStatic, IndexBuffer::kIndex32, header.indexCount);
    SetVertexData(vb, vertexData, 0, header.vertexCount);
    SetIndexData(ib, indexData, 0, header.indexCount);
}


void RenderLights(const Matrix44& view)
{
    BindVertexBuffer(lightVb);
    BindIndexBuffer(lightIb);

    glDepthMask(GL_FALSE);
    glEnable(GL_DEPTH_TEST);
    glDepthFunc(GL_GREATER);
    glCullFace(GL_FRONT);

    glEnable(GL_STENCIL_TEST);
    glDisable(GL_STENCIL_TEST);

//     glBeginQuery(GL_TIME_ELAPSED, query);
    for (size_t i=0; i<lightCount; ++i)
    {
        const Light& light = lights[i];

        Vector3 lightPos = MulPointTransform(light.position, view);

        GLint location = glGetUniformLocation(pointLightShader->GetHandle(), "position_extent");
        glUniform4f(location, lightPos.x, lightPos.y, lightPos.z, light.extent);
        location = glGetUniformLocation(pointLightShader->GetHandle(), "colour_radius");
        glUniform4f(location, light.colour.x, light.colour.y, light.colour.z, light.radius);

        glClear(GL_STENCIL_BUFFER_BIT);

//         // Stencil
//         glDisable(GL_CULL_FACE);
//         glStencilFunc(GL_ALWAYS, 0, 0xff);
//         glStencilOpSeparate(GL_FRONT, GL_KEEP, GL_KEEP, GL_DECR_WRAP);
//         glStencilOpSeparate(GL_BACK, GL_KEEP, GL_KEEP, GL_INCR_WRAP);
//         glColorMask(GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE);
//         glDrawElements(GL_TRIANGLES, lightIb->GetIndexCount(), GL_UNSIGNED_INT, NULL);
//         Assert((err=glGetError())==0, "GL Error");


        // Colour
        glEnable(GL_CULL_FACE);
        glStencilFunc(GL_EQUAL, 1, 0xff);
        glStencilOp(GL_KEEP, GL_KEEP, GL_KEEP);
        glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
        glDrawElements(GL_TRIANGLES, lightIb->GetIndexCount(), GL_UNSIGNED_INT, NULL);
        Assert((err=glGetError())==0, "GL Error");
    }

//     glEndQuery(GL_TIME_ELAPSED);

    glDepthFunc(GL_LESS);
    glCullFace(GL_BACK);
    glDisable(GL_BLEND);
    glDisable(GL_STENCIL_TEST);
}


void Render()
{
    float nearPlane = 0.01f;
    float farPlane = 100.0f;
    Matrix44 proj = PerspectiveProjection(60*kDegToRad, kAspectRatio, nearPlane, farPlane);
    Matrix44 view = TransformInverseRT(camera.GetTransform());
    Matrix44 world = Matrix44::kIdentity;
    Matrix44 worldView = world * view;
    Matrix44 worldViewProj = worldView * proj;
    GLint location;

    // ----------------------------------------------------
    // Reset basic state
    glEnable(GL_DEPTH_TEST);
    glDepthMask(GL_TRUE);
    glDepthFunc(GL_LESS);
    glEnable(GL_CULL_FACE);
    glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
    glClearDepth(1.0f);

    // ----------------------------------------------------
    // Draw geometry pass
    BindFrameBuffer(geometryFb);

    // Clear linear depth buffer to 1.0 (encoded)
    SetFrameBufferMask(true, false, false, false);
    glClearColor(1, 0, 0, 0);
    glClear(GL_COLOR_BUFFER_BIT);

    // Clear normal/gloss buffer
    SetFrameBufferMask(false, true, false, false);
    glClearColor(0, 0, 0, 0);
    glClear(GL_COLOR_BUFFER_BIT);

    SetFrameBufferMask(true, true, false, false);
    glClear(GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);

    BindShader(testShader);
    location = glGetUniformLocation(testShader->GetHandle(), "worldViewProj");
    glUniformMatrix4fv(location, 1, GL_FALSE, (GLfloat*)&worldViewProj);
    location = glGetUniformLocation(testShader->GetHandle(), "worldView");
    glUniformMatrix4fv(location, 1, GL_FALSE, (GLfloat*)&worldView);
    location = glGetUniformLocation(testShader->GetHandle(), "nearFarPlanes");
    glUniform4f(location, nearPlane, farPlane, 1/nearPlane, 1/farPlane);

    BindVertexBuffer(floorVb);
    glDrawArrays(GL_TRIANGLES, 0, 6);

    BindVertexBuffer(modelVb);
    BindIndexBuffer(modelIb);
    glDrawElements(GL_TRIANGLES, modelIb->GetIndexCount(), GL_UNSIGNED_INT, NULL);
    Assert((err=glGetError())==0, "GL Error");

    // ----------------------------------------------------
    // Light accumulation pass
    BindFrameBuffer(lightFb);
    glClearColor(0.2f, 0.2f, 0.2f, 0);
    glClear(GL_COLOR_BUFFER_BIT);

    glEnable(GL_CULL_FACE);
    glCullFace(GL_FRONT);

    glEnable(GL_BLEND);
    glBlendFunc(GL_ONE, GL_ONE);

    BindShader(pointLightShader);
    location = glGetUniformLocation(pointLightShader->GetHandle(), "linearDepthTex");
    glUniform1i(location, 0);
    location = glGetUniformLocation(pointLightShader->GetHandle(), "normalTex");
    glUniform1i(location, 1);

    location = glGetUniformLocation(pointLightShader->GetHandle(), "proj");
    glUniformMatrix4fv(location, 1, GL_FALSE, (GLfloat*)&proj);
    location = glGetUniformLocation(pointLightShader->GetHandle(), "nearFarPlanes");
    glUniform4f(location, nearPlane, farPlane, 1/nearPlane, 1/farPlane);

    BindTexture(linearDepthRt, 0);
    BindTexture(normalRt, 1);

    RenderLights(view);

    // ----------------------------------------------------
    // Resolve to back buffer
    BindFrameBuffer(NULL);

    glDisable(GL_DEPTH_TEST);
    glDepthMask(GL_FALSE);
    glDisable(GL_CULL_FACE);

    BindShader(fullscreenCopyShader);
    location = glGetUniformLocation(fullscreenCopyShader->GetHandle(), "src");
    glUniform1i(location, 0);

    BindTexture(lightRt, 0);

    BindVertexBuffer(fullscreenVb);
    glDrawArrays(GL_TRIANGLES, 0, 3);
    Assert((err=glGetError())==0, "GL Error");

    // ----------------------------------------------------
    // Aaand, relax
    SwapBuffers();
    Assert((err=glGetError())==0, "GL Error");

//     uint elapsed = 0;
//     glGetQueryObjectuiv(query, GL_QUERY_RESULT, &elapsed);
//     printf("%fms\n", (double)elapsed/1.e6);
}


void UpdateCamera()
{
    Vector3 translation = Vector3::kZero;
    if (Keyboard::IsKeyDown(Keyboard::kD))
        translation.x += 1;
    if (Keyboard::IsKeyDown(Keyboard::kA))
        translation.x -=1;
    if (Keyboard::IsKeyDown(Keyboard::kR))
        translation.y += 1;
    if (Keyboard::IsKeyDown(Keyboard::kF))
        translation.y -=1;
    if (Keyboard::IsKeyDown(Keyboard::kS))
        translation.z +=1;
    if (Keyboard::IsKeyDown(Keyboard::kW))
        translation.z -= 1;

    float speed = 0.1f;
    if (Keyboard::IsKeyDown(Keyboard::kLeftShift))
        speed *= 0.1f;

    Vector3 rotation = Vector3::kZero;
    if (Mouse::IsButtonDown(Mouse::kLeft))
    {
        int dx, dy;
        Mouse::GetMotion(dx, dy);
        rotation.x += 0.01f * dy;
        rotation.y += 0.01f * dx;
    }

    camera.UpdateTransform(speed*translation, rotation);
}


void UpdateLights()
{
    if (!isPaused)
    {
        Light* newLight;
        if (lightCount == kMaxLightCount)
        {
            lightBegin = (lightBegin + 1) % lightCount;
            newLight = &lights[lightBegin];
        }
        else
            newLight = &lights[lightCount++];

        newLight->position = Vector3(0, 2, 0);
        newLight->velocity = Vector3(rand()/(float)RAND_MAX - 0.5f, 2, rand()/(float)RAND_MAX-0.5f);
        newLight->colour = 0.1f * Vector3(rand()/(float)RAND_MAX, rand()/(float)RAND_MAX, rand()/(float)RAND_MAX);
        newLight->radius = 0.1f;
        newLight->extent = 3.0f;
        newLight->life = 1;

        for (size_t i=0; i<lightCount; ++i)
        {
            Light& light = lights[i];
            light.position += light.velocity / 60;
            light.velocity.y -= 9.8f/600;
        }
    }
}


void Tick()
{
	if (Keyboard::IsKeyDown(Keyboard::kEscape))
		isDone = true;
    if (Keyboard::WasKeyPressed(Keyboard::kSpace))
        isPaused = !isPaused;
    UpdateCamera();
    UpdateLights();
	Render();
}


int main(int, char*[])
{
	CreateDevice("Decals", kScreenWidth, kScreenHeight, 0, 0);
    glGetError();

	testShader = Shader::Compile("GeometryPass.glsl");
    fullscreenCopyShader = Shader::Compile("FullscreenCopy.glsl");
    pointLightShader = Shader::Compile("PointLight.glsl");

    Assert(testShader->IsValid(), testShader->GetError());
    Assert(fullscreenCopyShader->IsValid(), fullscreenCopyShader->GetError());
    Assert(pointLightShader->IsValid(), pointLightShader->GetError());

//     glGenQueries(1, &query);

    LoadMesh("Bunny.mesh", modelVb, modelIb);

    // Set up render buffers
    linearDepthRt = new Texture(Texture::kRgba8, kScreenWidth, kScreenHeight, Texture::kNearest, Texture::kNearest, NULL);
    normalRt = new Texture(Texture::kRgba8, kScreenWidth, kScreenHeight, Texture::kNearest, Texture::kNearest, NULL);
    lightRt = new Texture(Texture::kRgba16f, kScreenWidth, kScreenHeight, Texture::kNearest, Texture::kNearest, NULL);
    Assert((err=glGetError())==0, "GL Error");

    depthRt = new RenderTarget(RenderTarget::kDepthStencil, kScreenWidth, kScreenHeight);
    Assert((err=glGetError())==0, "GL Error");

    // Set up frame buffers
    geometryFb = new FrameBuffer(depthRt, linearDepthRt, normalRt);
    lightFb = new FrameBuffer(depthRt, lightRt);
    Assert((err=glGetError())==0, "GL Error");

    BindFrameBuffer(NULL);

    {
        VertexElement elements[] =
        {
            VertexElement(0, VertexElement::kPosition, 3, VertexElement::kFloat),
        };
        lightVb = new VertexBuffer(VertexBuffer::kStatic, elements, 1, sizeof(Vector3), 12);
        lightIb = new IndexBuffer(IndexBuffer::kStatic, IndexBuffer::kIndex32, 60);
        SetVertexData(lightVb, encloseUnitSphereVertices, 0, 12);
        SetIndexData(lightIb, triangleIndices, 0, 60);
    }

    {
        VertexElement elements[] =
        {
            VertexElement(0, VertexElement::kPosition, 3, VertexElement::kFloat),
            VertexElement(sizeof(Vector3), VertexElement::kNormal, 3, VertexElement::kFloat),
        };
        float s = 10;
        Vector3 vertices[] =
        {
            Vector3(-s, 0, -s), Vector3(0, 1, 0),
            Vector3(-s, 0,  s), Vector3(0, 1, 0),
            Vector3( s, 0,  s), Vector3(0, 1, 0),
            Vector3(-s, 0, -s), Vector3(0, 1, 0),
            Vector3( s, 0,  s), Vector3(0, 1, 0),
            Vector3( s, 0, -s), Vector3(0, 1, 0),
        };
        floorVb = new VertexBuffer(VertexBuffer::kStatic, elements, 2, 2*sizeof(Vector3), 6);
        SetVertexData(floorVb, vertices, 0, 6);
    }

    {
        VertexElement elements[] =
        {
            VertexElement(0, VertexElement::kPosition, 2, VertexElement::kFloat),
        };
        Vector2 vertices[] =
        {
            Vector2(0, 0),
            Vector2(2, 0),
            Vector2(0, 2),
        };
        fullscreenVb = new VertexBuffer(VertexBuffer::kStatic, elements, 1, sizeof(Vector2), 3);
        SetVertexData(fullscreenVb, vertices, 0, 3);
    }

	while (!isDone)
	{
		SDL_Event e;
		while (SDL_PollEvent(&e))
		{
			switch (e.type)
			{
			case SDL_QUIT:
				isDone = true;
				break;
			}
		}
		Keyboard::Update();
		Mouse::Update();
		Tick();
	}

    delete fullscreenVb;
    delete modelVb;
    delete floorVb;
    delete modelIb;
    delete testShader;
    delete fullscreenCopyShader;
    delete pointLightShader;
    delete lightVb;
    delete lightIb;

    delete linearDepthRt;
    delete normalRt;
    delete lightRt;
    delete depthRt;
    delete geometryFb;
    delete lightFb;

//     glDeleteQueries(1, &query);

	DestroyDevice();
	SDL_Quit();

	return 0;
}
